package game.game.editors;

import com.crunch.core.Actor;
import com.crunch.core.Input;
import com.crunch.core.Renderable;
import com.crunch.graphics.gpu.GpuBlendState;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.math.MathOps;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;
import com.crunch.physics.Segment;
import game.game.RoomDefinition;
import game.game.RoomState;
import util.Camera;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Allows editing of physics colliders.
 */
public class PhysicsEditor extends Actor implements Renderable {
	public PhysicsEditor() {
		super(-50);
	}

	@Override
	public void onCreate() {
		getEngine().window.renderer.register(this);
		commandQueue = new ConcurrentLinkedQueue<Runnable>();

		editor = new PhysicsEditorFrame();

		eventListener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				if (eventType == Input.MOUSE_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (ev.getButton() == MouseEvent.BUTTON2) {
						// camera drag
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							RoomState roomState = (RoomState) getGameState();
							int scale = roomState.getCoreState().getWindowScale();
							draggingCamera = true;
							dragX = ev.getX() / scale;
							dragY = ev.getY() / scale;
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							draggingCamera = false;
						}
					} else if (ev.getButton() == MouseEvent.BUTTON1 || ev.getButton() == MouseEvent.BUTTON3) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							mousePressed(ev.getX() / scale, ev.getY() / scale, ev.getButton());
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							mouseReleased(ev.getX() / scale, ev.getY() / scale, ev.getButton());
						}
					}
				} else if (eventType == Input.MOUSE_MOTION_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (draggingCamera) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						int newDragX = ev.getX() / scale;
						int newDragY = ev.getY() / scale;
						int dx = newDragX - dragX;
						int dy = dragY - newDragY; // flip y

						Camera camera = roomState.getCamera();
						int newX = (int) camera.getX() - dx;
						int newY = (int) camera.getY() - dy;

						// clamp to room size
						int xMin = newX - camera.getViewportWidth()/2;
						int yMin = newY - camera.getViewportHeight()/2;
						int xMax = xMin + camera.getViewportWidth();
						int yMax = yMin + camera.getViewportHeight();
						if (xMax > roomState.getRoomWidth()) {
							int diff = xMax - roomState.getRoomWidth();
							newX -= diff;
							xMin -= diff;
						}
						if (yMax > roomState.getRoomHeight()) {
							int diff = yMax - roomState.getRoomHeight();
							newY -= diff;
							yMin -= diff;
						}
						if (xMin < 0) {
							int diff = -xMin;
							newX += diff;
						}
						if (yMin < 0) {
							int diff = -yMin;
							newY += diff;
						}

						camera.set((float) newX, (float) newY, camera.getZoom());

						dragX = newDragX;
						dragY = newDragY;
					}
				}
			}
		};

		eventListener.eventMask = Input.MOUSE_EVENT | Input.MOUSE_MOTION_EVENT;
		getEngine().input.addEventListener(eventListener);
	}

	// initializes editor
	public void init(RoomDefinition roomDefinition) {
		this.roomDefinition = roomDefinition;
		editor.init();
	}

	@Override
	public void onUpdate() {
		// run all commands
		Runnable cmd;
		while ((cmd = commandQueue.poll()) != null) {
			cmd.run();
		}

		RoomState roomState = (RoomState) getGameState();

		if (addingCollider) {
			int vh = roomState.getCamera().getViewportHeight();
			int scale = roomState.getCoreState().getWindowScale();
			Point mousePosition = getEngine().input.getMousePosition();
			mousePosition.x /= scale;
			mousePosition.y /= scale;

			mousePosition.y = vh - mousePosition.y - 1;

			int cornerX = (int) roomState.getCamera().getX() - roomState.getCamera().getViewportWidth()/2;
			int cornerY = (int) roomState.getCamera().getY() - roomState.getCamera().getViewportHeight()/2;
			int x = mousePosition.x + cornerX + gridWidth/2;
			int y = mousePosition.y + cornerY + gridHeight/2;

			int offX = gridOffsetX % gridWidth;
			int offY = gridOffsetY % gridHeight;
			int alignedX = (x < offX) ? 0 : ((x - offX) / gridWidth) * gridWidth + offX;
			int alignedY = (y < offY) ? 0 : ((y - offY) / gridHeight) * gridHeight + offY;

			colliderEndX = alignedX;
			colliderEndY = alignedY;
		}

		if (removingColliders) {
			int vh = roomState.getCamera().getViewportHeight();
			int scale = roomState.getCoreState().getWindowScale();
			Point mousePosition = getEngine().input.getMousePosition();
			mousePosition.x /= scale;
			mousePosition.y /= scale;

			mousePosition.y = vh - mousePosition.y - 1;

			int cornerX = (int) roomState.getCamera().getX() - roomState.getCamera().getViewportWidth()/2;
			int cornerY = (int) roomState.getCamera().getY() - roomState.getCamera().getViewportHeight()/2;
			int x = mousePosition.x + cornerX;
			int y = mousePosition.y + cornerY;

			final float REMOVE_DISTANCE = 2.0f;
			Vector2f scP = new Vector2f((float) x, (float) y).divideAssign(RoomState.PHYSICS_SCALE);
			float dist = REMOVE_DISTANCE / RoomState.PHYSICS_SCALE;

			// determine which segments are overlapping and remove them
			int segCount = roomDefinition.getStaticColliderCount();
			for (int i = 0; i < segCount; ++i) {
				Segment segment = roomDefinition.getStaticCollider(i);

				// find distance from segment to scP
				Vector2f pA = segment.getA();
				Vector2f pB = segment.getB();
				Vector2f ab = pB.subtract(pA);
				Vector2f ap = scP.subtract(pA);
				// project
				float proj = MathOps.dot(ap, ab) / ab.magnitudeSquared();
				proj = MathOps.clamp(proj, 0.0f, 1.0f);
				Vector2f closest = ab.multiply(proj);
				float dist2 = MathOps.distanceSquared(ap, closest);
				if (dist2 <= dist*dist) {
					// remove the segment
					roomDefinition.removeStaticCollider(i);
					roomState.getPhysicsSimulation().removeStaticColliders(new int[] { i });
					--i;
					--segCount;
				}
			}
		}
	}

	@Override
	public void onDestroy() {
		getEngine().window.renderer.unregister(this);
		editor.dispose();

		getEngine().input.removeEventListener(eventListener);
	}

	private RoomDefinition roomDefinition;
	private boolean draggingCamera = false;
	private int dragX, dragY;

	private Input.EventListener eventListener;

	private boolean showGrid = false;
	private int gridWidth = 16;
	private int gridHeight = 16;
	private int gridOffsetX = 0;
	private int gridOffsetY = 0;

	private boolean addingCollider;
	private boolean removingColliders;
	private int colliderStartX;
	private int colliderStartY;
	private int colliderEndX;
	private int colliderEndY;

	private void mousePressed(int x, int y, int button) {
		if (button == MouseEvent.BUTTON1) {
			addingCollider = true;
			// convert to room coords
			RoomState roomState = (RoomState) getGameState();
			int vh = roomState.getCamera().getViewportHeight();
			y = vh - y - 1;
			int cornerX = (int) roomState.getCamera().getX() - roomState.getCamera().getViewportWidth()/2;
			int cornerY = (int) roomState.getCamera().getY() - roomState.getCamera().getViewportHeight()/2;
			x += cornerX + gridWidth/2;
			y += cornerY + gridHeight/2;

			int offX = gridOffsetX % gridWidth;
			int offY = gridOffsetY % gridHeight;
			int alignedX = (x < offX) ? 0 : ((x - offX) / gridWidth) * gridWidth + offX;
			int alignedY = (y < offY) ? 0 : ((y - offY) / gridHeight) * gridHeight + offY;

			colliderStartX = alignedX;
			colliderStartY = alignedY;
		} else if (button == MouseEvent.BUTTON3) {
			removingColliders = true;
		}
	}

	private void mouseReleased(int x, int y, int button) {
		RoomState roomState = (RoomState) getGameState();

		if (button == MouseEvent.BUTTON1) {
			addingCollider = false;

			if (colliderStartX != colliderEndX || colliderStartY != colliderEndY) {
				Segment segment = new Segment(
						new Vector2f((float) colliderStartX, (float) colliderStartY).divideAssign(RoomState.PHYSICS_SCALE),
						new Vector2f((float) colliderEndX, (float) colliderEndY).divideAssign(RoomState.PHYSICS_SCALE));
				roomDefinition.addStaticCollider(roomDefinition.getStaticColliderCount(), segment);
				roomState.getPhysicsSimulation().addStaticColliders(new Segment[] { segment });
			}
		} else if (button == MouseEvent.BUTTON3) {
			removingColliders = false;
		}
	}

	@Override
	public void render(GpuState gpuState) {
		RoomState roomState = (RoomState) getGameState();
		Camera camera = roomState.getCamera();

		if (addingCollider) {
			if (colliderStartX != colliderEndX || colliderStartY != colliderEndY) {
				Segment segment = new Segment(
						new Vector2f((float) colliderStartX, (float) colliderStartY).divideAssign(RoomState.PHYSICS_SCALE),
						new Vector2f((float) colliderEndX, (float) colliderEndY).divideAssign(RoomState.PHYSICS_SCALE));
				roomState.getPhysicsDebugRenderer().drawStaticCollider(gpuState, camera, segment, RoomState.PHYSICS_SCALE);
			}
		}

		GpuBlendState blendState = gpuState.createBlendState();

		// draw the screen grid
		if (showGrid && gridWidth > 3 && gridHeight > 3) {
			// determine offset using parallax
			int offsetX = gridOffsetX % gridWidth;
			int offsetY = gridOffsetY % gridHeight;

			Vector2f[] bounds = roomState.getCamera().getViewBounds();
			int xMin = (int) Math.floor(bounds[0].x());
			int yMin = (int) Math.floor(bounds[0].y());
			int xMax = (int) Math.ceil(bounds[1].x());
			int yMax = (int) Math.ceil(bounds[1].y());

			Vector2f minBound = new Vector2f();
			Vector2f maxBound = new Vector2f();
			Vector4f gridColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
			// draw grid with "inverted" colors
			blendState.setBlendFunction(GpuBlendState.BlendMode.INVERSE);
			blendState.bind();

			// draw each grid point
			// determine the first point to draw
			int startX = (xMin / gridWidth) * gridWidth + offsetX;
			int startY = (yMin / gridHeight) * gridHeight + offsetY;
			for (int y = startY; y <= yMax; y += gridHeight) {
				for (int x = startX; x <= xMax; x += gridWidth) {
					minBound.set((float) x, (float) y);
					maxBound.set(minBound.x() + 1.0f, minBound.y() + 1.0f);
					roomState.getSceneryRenderer().drawFilledRect(gpuState,
							camera.getViewProjectionMatrix(),
							minBound,
							maxBound,
							gridColor);
				}
			}
		}
	}

	@Override
	public int getDepth() {
		return Integer.MAX_VALUE - 10;
	}

	Queue<Runnable> commandQueue;

	PhysicsEditorFrame editor;
	private class PhysicsEditorFrame extends JFrame {
		private JCheckBox showGridCheckbox;
		private JSpinner gridWidthSpinner;
		private JSpinner gridHeightSpinner;
		private JSpinner gridXSpinner;
		private JSpinner gridYSpinner;

		private boolean performCallbacks;

		public PhysicsEditorFrame() {
			setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);

			setTitle("Physics editor");

			JPanel contentPanel = new JPanel();
			add(contentPanel);

			GroupLayout layout = new GroupLayout(contentPanel);
			contentPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			showGridCheckbox = new JCheckBox("Show grid");
			showGridCheckbox.setSelected(showGrid);
			JLabel gridSizeLabel = new JLabel("Grid size:");
			gridWidthSpinner = new JSpinner(new SpinnerNumberModel(gridWidth, 1, 512, 1));
			gridHeightSpinner = new JSpinner(new SpinnerNumberModel(gridHeight, 1, 512, 1));
			JLabel gridOffsetLabel = new JLabel("Grid offset:");
			gridXSpinner = new JSpinner(new SpinnerNumberModel(gridOffsetX, 0, 512, 1));
			gridYSpinner = new JSpinner(new SpinnerNumberModel(gridOffsetY, 0, 512, 1));

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(gridSizeLabel)
									.addComponent(gridOffsetLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(showGridCheckbox)
									.addGroup(layout.createSequentialGroup()
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(gridWidthSpinner)
													.addComponent(gridXSpinner))
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(gridHeightSpinner)
													.addComponent(gridYSpinner))))));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addComponent(showGridCheckbox)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(gridSizeLabel)
							.addComponent(gridWidthSpinner)
							.addComponent(gridHeightSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(gridOffsetLabel)
							.addComponent(gridXSpinner)
							.addComponent(gridYSpinner)));

			// a hack to account for border sizes
			Insets insets = getInsets();
			setMinimumSize(new Dimension(320 + insets.left + insets.right, getMinimumSize().height));
			setSize(320 + insets.left + insets.right, 768 + insets.top + insets.bottom);

			// add listeners
			ChangeListener gridSettingsListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}

					final boolean showGrid = showGridCheckbox.isSelected();
					final int gridWidth = (Integer) gridWidthSpinner.getValue();
					final int gridHeight = (Integer) gridHeightSpinner.getValue();
					final int gridOffsetX = (Integer) gridXSpinner.getValue();
					final int gridOffsetY = (Integer) gridYSpinner.getValue();

					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							PhysicsEditor.this.showGrid = showGrid;
							PhysicsEditor.this.gridWidth = gridWidth;
							PhysicsEditor.this.gridHeight = gridHeight;
							PhysicsEditor.this.gridOffsetX = gridOffsetX;
							PhysicsEditor.this.gridOffsetY = gridOffsetY;

							finishedCommand();
						}
					});
				}
			};

			showGridCheckbox.addChangeListener(gridSettingsListener);
			gridWidthSpinner.addChangeListener(gridSettingsListener);
			gridHeightSpinner.addChangeListener(gridSettingsListener);
			gridXSpinner.addChangeListener(gridSettingsListener);
			gridYSpinner.addChangeListener(gridSettingsListener);

			performCallbacks = true;

			setVisible(true);
		}

		private void init() {
			// .. nothing currently

			//SwingUtilities.invokeLater(new Runnable() {
			//	@Override
			//	public void run() {
			//		performCallbacks = false;
			//		try {
			//			// .. nothing currently
			//		} finally {
			//			performCallbacks = true;
			//		}
			//	}
			//});
		}

		private int commandsPending = 0;
		private void enqueueCommand(Runnable command) {
			commandQueue.add(command);
			++commandsPending;
		}
		private void finishedCommand() {
			finishedCommand(null);
		}
		private void finishedCommand(final Runnable uiCommand) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					--commandsPending;
					if (uiCommand != null) {
						uiCommand.run();
					}
				}
			});
		}
	}
}
